From 7252f4a75893868a22e43905170b6258c2e97767 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Mon, 19 Sep 2022 14:22:04 +0200 Subject: [PATCH] feat: CLI tooling to generate proposal JSONs (#13304) --- CHANGELOG.md | 1 + api/cosmos/nft/v1beta1/tx.pulsar.go | 22 +- client/prompts.go | 57 ++++ go.mod | 2 + go.sum | 5 + proto/cosmos/nft/v1beta1/tx.proto | 5 +- simapp/go.mod | 2 + simapp/go.sum | 5 + tests/go.mod | 2 + tests/go.sum | 5 + x/gov/README.md | 58 +++- x/gov/client/cli/prompt.go | 297 ++++++++++++++++++ x/gov/client/cli/tx.go | 1 + x/gov/client/cli/{parse.go => util.go} | 6 +- .../cli/{parse_test.go => util_test.go} | 0 x/group/README.md | 8 +- 16 files changed, 457 insertions(+), 19 deletions(-) create mode 100644 client/prompts.go create mode 100644 x/gov/client/cli/prompt.go rename x/gov/client/cli/{parse.go => util.go} (94%) rename x/gov/client/cli/{parse_test.go => util_test.go} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e737a3d307..94b5d614193a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,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. * (cli) [#13207](https://github.com/cosmos/cosmos-sdk/pull/13207) Reduce user's password prompts when calling keyring `List()` function * (x/authz) [#12648](https://github.com/cosmos/cosmos-sdk/pull/12648) Add an allow list, an optional list of addresses allowed to receive bank assets via authz MsgSend grant. * (sdk.Coins) [#12627](https://github.com/cosmos/cosmos-sdk/pull/12627) Make a Denoms method on sdk.Coins. diff --git a/api/cosmos/nft/v1beta1/tx.pulsar.go b/api/cosmos/nft/v1beta1/tx.pulsar.go index 2a7fbd181fe4..246a0a34c1e2 100644 --- a/api/cosmos/nft/v1beta1/tx.pulsar.go +++ b/api/cosmos/nft/v1beta1/tx.pulsar.go @@ -4,6 +4,7 @@ package nftv1beta1 import ( _ "cosmossdk.io/api/cosmos/msg/v1" fmt "fmt" + _ "github.com/cosmos/cosmos-proto" runtime "github.com/cosmos/cosmos-proto/runtime" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoiface "google.golang.org/protobuf/runtime/protoiface" @@ -1091,14 +1092,19 @@ var file_cosmos_nft_v1beta1_tx_proto_rawDesc = []byte{ 0x0a, 0x1b, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x6e, 0x66, 0x74, 0x2f, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2f, 0x74, 0x78, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x12, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x6e, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, - 0x31, 0x1a, 0x17, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x73, 0x67, 0x2f, 0x76, 0x31, - 0x2f, 0x6d, 0x73, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x75, 0x0a, 0x07, 0x4d, 0x73, - 0x67, 0x53, 0x65, 0x6e, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x49, 0x64, - 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, - 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x63, 0x65, - 0x69, 0x76, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x63, 0x65, + 0x31, 0x1a, 0x19, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, + 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x17, 0x63, 0x6f, + 0x73, 0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x73, 0x67, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x73, 0x67, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa9, 0x01, 0x0a, 0x07, 0x4d, 0x73, 0x67, 0x53, 0x65, 0x6e, + 0x64, 0x12, 0x19, 0x0a, 0x08, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x49, 0x64, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x30, 0x0a, 0x06, + 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x18, 0xd2, 0xb4, + 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x12, 0x34, + 0x0a, 0x08, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x18, 0xd2, 0xb4, 0x2d, 0x14, 0x63, 0x6f, 0x73, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x72, 0x65, 0x63, 0x65, 0x69, 0x76, 0x65, 0x72, 0x3a, 0x0b, 0x82, 0xe7, 0xb0, 0x2a, 0x06, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x22, 0x11, 0x0a, 0x0f, 0x4d, 0x73, 0x67, 0x53, 0x65, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x56, 0x0a, 0x03, 0x4d, 0x73, 0x67, 0x12, 0x48, 0x0a, 0x04, 0x53, 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 75fdfcc44645..10de03577788 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ 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/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.13.0 @@ -75,6 +76,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 diff --git a/go.sum b/go.sum index df52e83ad8b5..8ca743c30f78 100644 --- a/go.sum +++ b/go.sum @@ -147,8 +147,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= @@ -560,6 +563,8 @@ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0Q github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= diff --git a/proto/cosmos/nft/v1beta1/tx.proto b/proto/cosmos/nft/v1beta1/tx.proto index 52bbeb0104b9..0637cd8d8956 100644 --- a/proto/cosmos/nft/v1beta1/tx.proto +++ b/proto/cosmos/nft/v1beta1/tx.proto @@ -3,6 +3,7 @@ package cosmos.nft.v1beta1; option go_package = "github.com/cosmos/cosmos-sdk/x/nft"; +import "cosmos_proto/cosmos.proto"; import "cosmos/msg/v1/msg.proto"; // Msg defines the nft Msg service. @@ -24,10 +25,10 @@ message MsgSend { string id = 2; // sender is the address of the owner of nft - string sender = 3; + string sender = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // receiver is the receiver address of nft - string receiver = 4; + string receiver = 4 [(cosmos_proto.scalar) = "cosmos.AddressString"]; } // MsgSendResponse defines the Msg/Send response type. message MsgSendResponse {} \ No newline at end of file diff --git a/simapp/go.mod b/simapp/go.mod index 8468a82199d4..18f1a9287316 100644 --- a/simapp/go.mod +++ b/simapp/go.mod @@ -37,6 +37,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/cockroachdb/apd/v2 v2.0.2 // indirect github.com/coinbase/rosetta-sdk-go v0.8.0 // indirect github.com/confio/ics23/go v0.7.0 // indirect @@ -97,6 +98,7 @@ require ( github.com/lib/pq v1.10.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/magiconair/properties v1.8.6 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect diff --git a/simapp/go.sum b/simapp/go.sum index c6bbc67733be..5a40379a4064 100644 --- a/simapp/go.sum +++ b/simapp/go.sum @@ -147,8 +147,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= @@ -548,6 +551,8 @@ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0Q github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= diff --git a/tests/go.mod b/tests/go.mod index aa044c5887fa..0f6ff6d29198 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -36,6 +36,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/cockroachdb/apd/v2 v2.0.2 // indirect github.com/coinbase/rosetta-sdk-go v0.8.0 // indirect github.com/confio/ics23/go v0.7.0 // indirect @@ -96,6 +97,7 @@ require ( github.com/lib/pq v1.10.6 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/magiconair/properties v1.8.6 // indirect + github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect diff --git a/tests/go.sum b/tests/go.sum index beac75ee56c8..b3ef1f3ceca2 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -147,8 +147,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= @@ -550,6 +553,8 @@ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0Q github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= diff --git a/x/gov/README.md b/x/gov/README.md index 7a5344dba177..286c1ff56e16 100644 --- a/x/gov/README.md +++ b/x/gov/README.md @@ -35,11 +35,15 @@ The following specification uses *ATOM* as the native staking token. The module can be adapted to any Proof-Of-Stake blockchain by replacing *ATOM* with the native staking token of the chain. +* [`x/gov`](#xgov) + * [Abstract](#abstract) + * [Contents](#contents) * [Concepts](#concepts) * [Proposal submission](#proposal-submission) * [Right to submit a proposal](#right-to-submit-a-proposal) * [Proposal Messages](#proposal-messages) * [Deposit](#deposit) + * [Deposit refund and burn](#deposit-refund-and-burn) * [Vote](#vote) * [Participants](#participants) * [Voting period](#voting-period) @@ -51,8 +55,11 @@ staking token of the chain. * [Validator’s punishment for non-voting](#validators-punishment-for-non-voting) * [Governance address](#governance-address) * [Software Upgrade](#software-upgrade) + * [Signal](#signal) + * [Switch](#switch) * [State](#state) * [Proposals](#proposals) + * [Writing a module that uses governance](#writing-a-module-that-uses-governance) * [Parameters and base types](#parameters-and-base-types) * [DepositParams](#depositparams) * [VotingParams](#votingparams) @@ -75,11 +82,48 @@ staking token of the chain. * [MsgDeposit](#msgdeposit) * [Future Improvements](#future-improvements) * [Parameters](#parameters) + * [SubKeys](#subkeys) * [Client](#client) * [CLI](#cli) + * [Query](#query) + * [deposit](#deposit-3) + * [deposits](#deposits) + * [param](#param) + * [params](#params) + * [proposal](#proposal) + * [proposals](#proposals-1) + * [proposer](#proposer) + * [tally](#tally) + * [vote](#vote-2) + * [votes](#votes) + * [Transactions](#transactions) + * [deposit](#deposit-4) + * [draft-proposal](#draft-proposal) + * [submit-proposal](#submit-proposal) + * [submit-legacy-proposal](#submit-legacy-proposal) + * [vote](#vote-3) + * [weighted-vote](#weighted-vote) * [gRPC](#grpc) + * [Proposal](#proposal-1) + * [Proposals](#proposals-2) + * [Vote](#vote-4) + * [Votes](#votes-1) + * [Params](#params-1) + * [Deposit](#deposit-5) + * [deposits](#deposits-1) + * [TallyResult](#tallyresult) * [REST](#rest) + * [proposal](#proposal-2) + * [proposals](#proposals-3) + * [voter vote](#voter-vote) + * [votes](#votes-2) + * [params](#params-2) + * [deposits](#deposits-2) + * [proposal deposits](#proposal-deposits) + * [tally](#tally-1) * [Metadata](#metadata) + * [Proposal](#proposal-3) + * [Vote](#vote-5) @@ -1149,6 +1193,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](#metadata). + +```bash +simd tx gov draft-proposal +``` + #### submit-proposal The `submit-proposal` command allows users to submit a governance proposal along with some messages and metadata. @@ -2612,8 +2666,8 @@ Location: off-chain as json object stored on IPFS (mirrors [group proposal](../. "authors": "", "summary": "", "details": "", - "proposalForumURL": "", - "voteOptionContext": "", + "proposal_forum_url": "", + "vote_option_context": "", } ``` diff --git a/x/gov/client/cli/prompt.go b/x/gov/client/cli/prompt.go new file mode 100644 index 000000000000..b997f5a4a8b9 --- /dev/null +++ b/x/gov/client/cli/prompt.go @@ -0,0 +1,297 @@ +package cli + +import ( + "encoding/json" + "fmt" + "os" + "reflect" + "sort" + "strconv" + "strings" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + + "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 ( + proposalText = "text" + proposalOther = "other" + draftProposalFileName = "draft_proposal.json" + draftMetadataFileName = "draft_metadata.json" +) + +// 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 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) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + } + + for i := 0; i < v.NumField(); i++ { + 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 + } + + // create prompts + prompt := promptui.Prompt{ + Label: fmt.Sprintf("Enter %s %s", namePrefix, strings.ToLower(client.CamelCaseToString(v.Type().Field(i).Name))), + Validate: client.ValidatePromptNotEmpty, + } + + 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 + prompt.Default = authtypes.NewModuleAddress(types.ModuleName).String() + prompt.Validate = client.ValidatePromptAddress + } + + if strings.Contains(fieldName, "addr") || + strings.Contains(fieldName, "sender") || + strings.Contains(fieldName, "voter") || + strings.Contains(fieldName, "depositor") || + strings.Contains(fieldName, "granter") || + strings.Contains(fieldName, "grantee") || + strings.Contains(fieldName, "recipient") { + prompt.Validate = client.ValidatePromptAddress + } + + result, err := prompt.Run() + if err != nil { + return data, fmt.Errorf("failed to prompt for %s: %w", fieldName, err) + } + + switch v.Field(i).Kind() { + case reflect.String: + v.Field(i).SetString(result) + case reflect.Int: + resultInt, _ := strconv.Atoi(result) + v.Field(i).SetInt(int64(resultInt)) + default: + // skip other types + // possibly in the future we can add more types (like slices) + continue + } + } + + return data, nil +} + +type proposalTypes struct { + Type string + MsgType string + Msg sdk.Msg +} + +// Prompt the proposal type values and return the proposal and its metadata +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.Metadata = "ipfs://CID" + + // set deposit + depositPrompt := promptui.Prompt{ + Label: "Enter proposal deposit", + Validate: client.ValidatePromptCoins, + } + proposal.Deposit, err = depositPrompt.Run() + if err != nil { + return nil, metadata, fmt.Errorf("failed to set proposal deposit: %w", err) + } + + if p.Msg == nil { + return proposal, metadata, nil + } + + // set messages field + result, err := Prompt(p.Msg, "msg") + if err != nil { + return nil, metadata, fmt.Errorf("failed to set proposal message: %w", err) + } + + message, err := cdc.MarshalInterfaceJSON(result) + if err != nil { + return nil, metadata, fmt.Errorf("failed to marshal proposal message: %w", err) + } + proposal.Messages = append(proposal.Messages, message) + return proposal, metadata, nil +} + +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 + }, +} + +func getProposalTypes() []string { + types := make([]string, len(supportedProposalTypes)) + for i, p := range supportedProposalTypes { + types[i] = p.Type + } + return types +} + +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 nil, 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 msg, nil +} + +// NewCmdDraftProposal let a user generate a draft proposal. +func NewCmdDraftProposal() *cobra.Command { + cmd := &cobra.Command{ + Use: "draft-proposal", + 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) + if err != nil { + return err + } + + // prompt proposal type + proposalTypesPrompt := promptui.Select{ + Label: "Select proposal type", + Items: getProposalTypes(), + } + + _, proposalType, err := proposalTypesPrompt.Run() + if err != nil { + return fmt.Errorf("failed to prompt proposal types: %w", err) + } + + var proposal proposalTypes + for _, p := range supportedProposalTypes { + if strings.EqualFold(p.Type, proposalType) { + proposal = p + break + } + } + + // create any proposal type + if proposal.Type == proposalOther { + // prompt proposal type + msgPrompt := promptui.Select{ + Label: "Select proposal message type:", + Items: func() []string { + msgs := clientCtx.InterfaceRegistry.ListImplementations(sdk.MsgInterfaceProtoName) + sort.Strings(msgs) + return msgs + }(), + } + + _, result, err := msgPrompt.Run() + if err != nil { + return fmt.Errorf("failed to prompt proposal types: %w", err) + } + + proposal.MsgType = result + } + + if proposal.MsgType != "" { + proposal.Msg, err = getProposalMsg(clientCtx.Codec, proposal.MsgType) + if err != nil { + // should never happen + panic(err) + } + } + + prop, metadata, err := proposal.Prompt(clientCtx.Codec) + if err != nil { + return err + } + + if err := writeFile(draftMetadataFileName, metadata); err != nil { + return err + } + + if err := writeFile(draftProposalFileName, prop); err != nil { + return err + } + + 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) + + return cmd +} + +func writeFile(fileName string, input any) error { + raw, err := json.MarshalIndent(input, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal proposal: %w", err) + } + + if err := os.WriteFile(fileName, raw, 0o600); err != nil { + return err + } + + return nil +} diff --git a/x/gov/client/cli/tx.go b/x/gov/client/cli/tx.go index 85e821ddebee..f18c14874984 100644 --- a/x/gov/client/cli/tx.go +++ b/x/gov/client/cli/tx.go @@ -70,6 +70,7 @@ func NewTxCmd(legacyPropCmds []*cobra.Command) *cobra.Command { NewCmdVote(), NewCmdWeightedVote(), NewCmdSubmitProposal(), + NewCmdDraftProposal(), // Deprecated cmdSubmitLegacyProp, diff --git a/x/gov/client/cli/parse.go b/x/gov/client/cli/util.go similarity index 94% rename from x/gov/client/cli/parse.go rename to x/gov/client/cli/util.go index a43d7f868cea..64517fab22c6 100644 --- a/x/gov/client/cli/parse.go +++ b/x/gov/client/cli/util.go @@ -77,9 +77,9 @@ func parseSubmitLegacyProposalFlags(fs *pflag.FlagSet) (*legacyProposal, error) // 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 - Metadata string - Deposit string + Messages []json.RawMessage `json:"messages,omitempty"` + Metadata string `json:"metadata"` + Deposit string `json:"deposit"` } func parseSubmitProposal(cdc codec.Codec, path string) ([]sdk.Msg, string, sdk.Coins, error) { diff --git a/x/gov/client/cli/parse_test.go b/x/gov/client/cli/util_test.go similarity index 100% rename from x/gov/client/cli/parse_test.go rename to x/gov/client/cli/util_test.go diff --git a/x/group/README.md b/x/group/README.md index f1aab2cdeaee..4ca2fae74c0d 100644 --- a/x/group/README.md +++ b/x/group/README.md @@ -2073,8 +2073,8 @@ Location: off-chain as json object stored on IPFS (mirrors [gov proposal](../../ "authors": "", "summary": "", "details": "", - "proposalForumURL": "", - "voteOptionContext": "", + "proposal_forum_url": "", + "vote_option_context": "", } ``` @@ -2096,8 +2096,8 @@ Location: off-chain as json object stored on IPFS { "name": "", "description": "", - "groupWebsiteURL": "", - "groupForumURL": "", + "group_website_url": "", + "group_forum_url": "", } ```