From 4f8a750fcf0b06bade80dcbee67d36d00e896c24 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Wed, 21 Sep 2022 12:50:06 +0200 Subject: [PATCH 1/5] feat: add `draft-proposal` for x/group --- x/gov/client/cli/prompt.go | 65 ++++++-------- x/gov/types/metadata.go | 12 +++ x/group/client/cli/prompt.go | 160 +++++++++++++++++++++++++++++++++++ x/group/client/cli/tx.go | 1 + x/group/client/cli/util.go | 4 +- 5 files changed, 203 insertions(+), 39 deletions(-) create mode 100644 x/gov/types/metadata.go create mode 100644 x/group/client/cli/prompt.go diff --git a/x/gov/client/cli/prompt.go b/x/gov/client/cli/prompt.go index 337808bb3ea7..073b2abe473a 100644 --- a/x/gov/client/cli/prompt.go +++ b/x/gov/client/cli/prompt.go @@ -27,17 +27,6 @@ const ( 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 " @@ -115,21 +104,22 @@ func Prompt[T any](data T, namePrefix string) (T, error) { return data, nil } -type proposalTypes struct { - Type string +type proposalType struct { + Name 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) { +func (p *proposalType) Prompt(cdc codec.Codec) (*proposal, types.ProposalMetadata, error) { proposal := &proposal{} // set metadata - metadata, err := Prompt(ProposalMetadata{}, "proposal") + metadata, err := Prompt(types.ProposalMetadata{}, "proposal") if err != nil { return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err) } + // the metadata must be saved on IPFS, set placeholder proposal.Metadata = "ipfs://CID" // set deposit @@ -160,38 +150,39 @@ func (p *proposalTypes) Prompt(cdc codec.Codec) (*proposal, ProposalMetadata, er return proposal, metadata, nil } -var supportedProposalTypes = []proposalTypes{ +var suggestedProposalTypes = []proposalType{ { - Type: proposalText, + Name: proposalText, MsgType: "", // no message for text proposal }, { - Type: "community-pool-spend", + Name: "community-pool-spend", MsgType: "/cosmos.distribution.v1beta1.MsgCommunityPoolSpend", }, { - Type: "software-upgrade", + Name: "software-upgrade", MsgType: "/cosmos.upgrade.v1beta1.MsgSoftwareUpgrade", }, { - Type: "cancel-software-upgrade", + Name: "cancel-software-upgrade", MsgType: "/cosmos.upgrade.v1beta1.MsgCancelUpgrade", }, { - Type: proposalOther, + Name: 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 +func getProposalSuggestions() []string { + types := make([]string, len(suggestedProposalTypes)) + for i, p := range suggestedProposalTypes { + types[i] = p.Name } return types } -func getProposalMsg(cdc codec.Codec, input string) (sdk.Msg, error) { +// GetProposalMsg returns the proposal message type from a string +func GetProposalMsg(cdc codec.Codec, input string) (sdk.Msg, error) { var msg sdk.Msg bz, err := json.Marshal(struct { Type string `json:"@type"` @@ -213,7 +204,7 @@ func getProposalMsg(cdc codec.Codec, input string) (sdk.Msg, error) { 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.", + Short: "Generate a draft proposal json file. The generated proposal json contains only one message (skeleton).", SilenceUsage: true, RunE: func(cmd *cobra.Command, _ []string) error { clientCtx, err := client.GetClientTxContext(cmd) @@ -224,24 +215,24 @@ func NewCmdDraftProposal() *cobra.Command { // prompt proposal type proposalTypesPrompt := promptui.Select{ Label: "Select proposal type", - Items: getProposalTypes(), + Items: getProposalSuggestions(), } - _, proposalType, err := proposalTypesPrompt.Run() + _, selectedProposalType, 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) { + var proposal proposalType + for _, p := range suggestedProposalTypes { + if strings.EqualFold(p.Name, selectedProposalType) { proposal = p break } } // create any proposal type - if proposal.Type == proposalOther { + if proposal.Name == proposalOther { // prompt proposal type msgPrompt := promptui.Select{ Label: "Select proposal message type:", @@ -261,23 +252,23 @@ func NewCmdDraftProposal() *cobra.Command { } if proposal.MsgType != "" { - proposal.Msg, err = getProposalMsg(clientCtx.Codec, 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) + result, metadata, err := proposal.Prompt(clientCtx.Codec) if err != nil { return err } - if err := writeFile(draftMetadataFileName, metadata); err != nil { + if err := writeFile(draftProposalFileName, result); err != nil { return err } - if err := writeFile(draftProposalFileName, prop); err != nil { + if err := writeFile(draftMetadataFileName, metadata); err != nil { return err } diff --git a/x/gov/types/metadata.go b/x/gov/types/metadata.go new file mode 100644 index 000000000000..8b7b961f2229 --- /dev/null +++ b/x/gov/types/metadata.go @@ -0,0 +1,12 @@ +package types + +// 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"` +} diff --git a/x/group/client/cli/prompt.go b/x/group/client/cli/prompt.go new file mode 100644 index 000000000000..cdf11e2b0b29 --- /dev/null +++ b/x/group/client/cli/prompt.go @@ -0,0 +1,160 @@ +package cli + +import ( + "encoding/json" + "fmt" + "os" + "sort" + + "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" + govcli "github.com/cosmos/cosmos-sdk/x/gov/client/cli" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" +) + +const ( + proposalText = "text" + proposalOther = "other" + draftProposalFileName = "draft_group_proposal.json" + draftMetadataFileName = "draft_group_metadata.json" +) + +type proposalType struct { + Name string + Msg sdk.Msg +} + +// Prompt the proposal type values and return the proposal and its metadata +func (p *proposalType) Prompt(cdc codec.Codec) (*Proposal, govtypes.ProposalMetadata, error) { + proposal := &Proposal{} + + // set metadata + metadata, err := govcli.Prompt(govtypes.ProposalMetadata{}, "proposal") + if err != nil { + return nil, metadata, fmt.Errorf("failed to set proposal metadata: %w", err) + } + // the metadata must be saved on IPFS, set placeholder + proposal.Metadata = "ipfs://CID" + + // set group policy address + policyAddressPrompt := promptui.Prompt{ + Label: "Enter group policy address", + Validate: client.ValidatePromptAddress, + } + groupPolicyAddress, err := policyAddressPrompt.Run() + if err != nil { + return nil, metadata, fmt.Errorf("failed to set group policy address: %w", err) + } + proposal.GroupPolicyAddress = groupPolicyAddress + + if p.Msg == nil { + return proposal, metadata, nil + } + + // set messages field + result, err := govcli.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 +} + +// 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 (skeleton).", + 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: []string{proposalText, proposalOther}, + } + + _, selectedProposalType, err := proposalTypesPrompt.Run() + if err != nil { + return fmt.Errorf("failed to prompt proposal types: %w", err) + } + + var proposal *proposalType + switch selectedProposalType { + case proposalText: + proposal = &proposalType{Name: proposalText} + case proposalOther: + // prompt proposal type + proposal = &proposalType{Name: proposalOther} + 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.Msg, err = govcli.GetProposalMsg(clientCtx.Codec, result) + if err != nil { + // should never happen + panic(err) + } + default: + panic("unexpected proposal type") + } + + result, metadata, err := proposal.Prompt(clientCtx.Codec) + if err != nil { + return err + } + + if err := writeFile(draftProposalFileName, result); err != nil { + return err + } + + if err := writeFile(draftMetadataFileName, metadata); 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/group/client/cli/tx.go b/x/group/client/cli/tx.go index 4d8c46628df8..54a7efb7c2a6 100644 --- a/x/group/client/cli/tx.go +++ b/x/group/client/cli/tx.go @@ -45,6 +45,7 @@ func TxCmd(name string) *cobra.Command { MsgVoteCmd(), MsgExecCmd(), MsgLeaveGroupCmd(), + NewCmdDraftProposal(), ) return txCmd diff --git a/x/group/client/cli/util.go b/x/group/client/cli/util.go index 7afc43873f4b..9d9c0ecf5d59 100644 --- a/x/group/client/cli/util.go +++ b/x/group/client/cli/util.go @@ -61,9 +61,9 @@ func execFromString(execStr string) group.Exec { type Proposal struct { GroupPolicyAddress string `json:"group_policy_address"` // Messages defines an array of sdk.Msgs proto-JSON-encoded as Anys. - Messages []json.RawMessage `json:"messages"` + Messages []json.RawMessage `json:"messages,omitempty"` Metadata string `json:"metadata"` - Proposers []string `json:"proposers"` + Proposers []string `json:"proposers,omitempty"` } func getCLIProposal(path string) (Proposal, error) { From be41aabeead0553e987afd7147bb5019f349c0bc Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Wed, 21 Sep 2022 12:51:42 +0200 Subject: [PATCH 2/5] add changelog --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcd05e5b65d5..a8fdbd060315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,8 @@ 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) [#13353](https://github.com/cosmos/cosmos-sdk/pull/13353) Add `tx group draft-proposal` command for generating group proposal JSONs (skeleton). +* (cli) [#13304](https://github.com/cosmos/cosmos-sdk/pull/13304) Add `tx gov draft-proposal` command for generating proposal JSONs (skeleton). * (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. @@ -487,7 +488,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ * (x/params) [#12724](https://github.com/cosmos/cosmos-sdk/pull/12724) Add `GetParamSetIfExists` function to params `Subspace` to prevent panics on breaking changes. * [#12668](https://github.com/cosmos/cosmos-sdk/pull/12668) Add `authz_msg_index` event attribute to message events emitted when executing via `MsgExec` through `x/authz`. * [#12697](https://github.com/cosmos/cosmos-sdk/pull/12697) Upgrade IAVL to v0.19.0 with fast index and error propagation. NOTE: first start will take a while to propagate into new model. - - Note: after upgrading to this version it may take up to 15 minutes to migrate from 0.17 to 0.19. This time is used to create the fast cache introduced into IAVL for performance + * Note: after upgrading to this version it may take up to 15 minutes to migrate from 0.17 to 0.19. This time is used to create the fast cache introduced into IAVL for performance * [#12784](https://github.com/cosmos/cosmos-sdk/pull/12784) Upgrade Tendermint to 0.34.20. * (x/bank) [#12674](https://github.com/cosmos/cosmos-sdk/pull/12674) Add convenience function `CreatePrefixedAccountStoreKey()` to construct key to access account's balance for a given denom. From 7889dee6ec56a059fcb884cf658e746af4d23a13 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Wed, 21 Sep 2022 12:56:44 +0200 Subject: [PATCH 3/5] extract useful function --- go.mod | 2 +- orm/go.mod | 2 +- types/tx_msg.go | 23 +++++++++++++++++++++++ x/gov/client/cli/prompt.go | 21 +-------------------- x/group/client/cli/prompt.go | 2 +- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index 71f91e5c61bb..7e064256defe 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/armon/go-metrics v0.4.1 github.com/bgentry/speakeasy v0.1.0 github.com/btcsuite/btcd v0.22.1 + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/cockroachdb/apd/v2 v2.0.2 github.com/coinbase/rosetta-sdk-go v0.8.0 github.com/confio/ics23/go v0.7.0 @@ -76,7 +77,6 @@ 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/orm/go.mod b/orm/go.mod index 66906e38350c..e1b1aed00c16 100644 --- a/orm/go.mod +++ b/orm/go.mod @@ -12,6 +12,7 @@ require ( github.com/regen-network/gocuke v0.6.2 github.com/stretchr/testify v1.8.0 github.com/tendermint/tm-db v0.6.7 + golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b google.golang.org/grpc v1.49.0 google.golang.org/protobuf v1.28.1 gotest.tools/v3 v3.3.0 @@ -49,7 +50,6 @@ require ( github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect go.etcd.io/bbolt v1.3.6 // indirect - golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b // indirect golang.org/x/net v0.0.0-20220726230323-06994584191e // indirect golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect golang.org/x/text v0.3.7 // indirect diff --git a/types/tx_msg.go b/types/tx_msg.go index e76138cd7ef5..41c647e57de5 100644 --- a/types/tx_msg.go +++ b/types/tx_msg.go @@ -1,8 +1,12 @@ package types import ( + "encoding/json" + fmt "fmt" + "github.com/cosmos/gogoproto/proto" + "github.com/cosmos/cosmos-sdk/codec" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" ) @@ -79,3 +83,22 @@ type TxEncoder func(tx Tx) ([]byte, error) func MsgTypeURL(msg Msg) string { return "/" + proto.MessageName(msg) } + +// GetMsgFromTypeURL returns a `sdk.Msg` message type from a type URL +func GetMsgFromTypeURL(cdc codec.Codec, input string) (Msg, error) { + var msg 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 determine sdk.Msg for %s URL : %w", input, err) + } + + return msg, nil +} diff --git a/x/gov/client/cli/prompt.go b/x/gov/client/cli/prompt.go index 073b2abe473a..5e3ef4e2e971 100644 --- a/x/gov/client/cli/prompt.go +++ b/x/gov/client/cli/prompt.go @@ -181,25 +181,6 @@ func getProposalSuggestions() []string { return types } -// GetProposalMsg returns the proposal message type from a string -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{ @@ -252,7 +233,7 @@ func NewCmdDraftProposal() *cobra.Command { } if proposal.MsgType != "" { - proposal.Msg, err = GetProposalMsg(clientCtx.Codec, proposal.MsgType) + proposal.Msg, err = sdk.GetMsgFromTypeURL(clientCtx.Codec, proposal.MsgType) if err != nil { // should never happen panic(err) diff --git a/x/group/client/cli/prompt.go b/x/group/client/cli/prompt.go index cdf11e2b0b29..b8e446df56a9 100644 --- a/x/group/client/cli/prompt.go +++ b/x/group/client/cli/prompt.go @@ -113,7 +113,7 @@ func NewCmdDraftProposal() *cobra.Command { return fmt.Errorf("failed to prompt proposal types: %w", err) } - proposal.Msg, err = govcli.GetProposalMsg(clientCtx.Codec, result) + proposal.Msg, err = sdk.GetMsgFromTypeURL(clientCtx.Codec, result) if err != nil { // should never happen panic(err) From 5833da88bec7fcf019cf864d2fab8ca9e060fca4 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Wed, 21 Sep 2022 16:14:15 +0200 Subject: [PATCH 4/5] add `GetMsgFromTypeURL` tests --- types/tx_msg_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/types/tx_msg_test.go b/types/tx_msg_test.go index 7e72035d5175..9d8200af93df 100644 --- a/types/tx_msg_test.go +++ b/types/tx_msg_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -33,3 +34,12 @@ func (s *testMsgSuite) TestMsg() { func (s *testMsgSuite) TestMsgTypeURL() { s.Require().Equal("/testdata.TestMsg", sdk.MsgTypeURL(new(testdata.TestMsg))) } + +func (s *testMsgSuite) TestMsgTypeURLPanic() { + msg := new(testdata.TestMsg) + cdc := codec.NewProtoCodec(testdata.NewTestInterfaceRegistry()) + + result, err := sdk.GetMsgFromTypeURL(cdc, "/testdata.TestMsg") + s.Require().NoError(err) + s.Require().Equal(msg, result) +} From c187e55b73fa14ce8d7f5fc08eca1f8867225a5b Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Wed, 21 Sep 2022 16:15:00 +0200 Subject: [PATCH 5/5] renaming --- types/tx_msg_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/tx_msg_test.go b/types/tx_msg_test.go index 9d8200af93df..0366d4fb14bc 100644 --- a/types/tx_msg_test.go +++ b/types/tx_msg_test.go @@ -35,7 +35,7 @@ func (s *testMsgSuite) TestMsgTypeURL() { s.Require().Equal("/testdata.TestMsg", sdk.MsgTypeURL(new(testdata.TestMsg))) } -func (s *testMsgSuite) TestMsgTypeURLPanic() { +func (s *testMsgSuite) TestGetMsgFromTypeURL() { msg := new(testdata.TestMsg) cdc := codec.NewProtoCodec(testdata.NewTestInterfaceRegistry())