feat: add draft-proposal for x/group (#13353)

* feat: add `draft-proposal` for x/group

* add changelog

* extract useful function

* add `GetMsgFromTypeURL` tests
This commit is contained in:
Julien Robert 2022-09-22 00:53:02 +02:00 committed by GitHub
parent fc32ef1c9c
commit 7eb259fd86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 237 additions and 58 deletions

View File

@ -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.

View File

@ -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
}

View File

@ -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) TestGetMsgFromTypeURL() {
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)
}

View File

@ -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 <namePrefix> <field>"
@ -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,60 +150,42 @@ 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) {
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.",
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 +196,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 +233,23 @@ 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)
}
}
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
}

12
x/gov/types/metadata.go Normal file
View File

@ -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"`
}

View File

@ -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 = sdk.GetMsgFromTypeURL(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
}

View File

@ -45,6 +45,7 @@ func TxCmd(name string) *cobra.Command {
MsgVoteCmd(),
MsgExecCmd(),
MsgLeaveGroupCmd(),
NewCmdDraftProposal(),
)
return txCmd

View File

@ -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) {